@abraca/convert 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # @abraca/convert
2
+
3
+ The canonical Markdown ↔ Yjs/TipTap round-trip converter — shared by cou-sh, abracadabra-nuxt, and `@abraca/mcp` instead of each vendoring its own copy. **Zero runtime dependencies** (`yjs` peer-only).
4
+
5
+ ## Documentation
6
+
7
+ Full, code-derived documentation lives in [`docs/`](docs/) — the node/mark spec, the
8
+ universal-meta spec, the conversion API, diff + file-blocks, and a precise
9
+ fidelity/gotchas reference. It is the source of truth (the old `SPEC.md` was untrusted
10
+ and removed).
11
+
12
+ ## API
13
+
14
+ ```ts
15
+ import { populateYDocFromMarkdown, yjsToMarkdown } from "@abraca/convert";
16
+ populateYDocFromMarkdown(doc.getXmlFragment("default"), markdown);
17
+ const { title, markdown } = /* via */ yjsToMarkdown(fragment, label);
18
+ ```
19
+
20
+ Fragment shape: `[documentHeader, documentMeta, ...body]`. Also `populateYDocFromHtml` (needs a global `DOMParser`), `yjsToHtml`, `yjsToPlainText`, structural `diff.ts`, and the FS-sync `file-blocks/` sidecar.
21
+
22
+ ::
23
+
24
+ > **Attach-before-fill is load-bearing** — `Y.XmlText.insert` on a detached node reverses text (YATA clock 0). Coverage is partial: several spec nodes/marks have no runtime branch, and frontmatter handles a ~13-key subset. See [`docs/4.reference/`](docs/4.reference/1.fidelity-and-gotchas.md).
25
+
26
+ ## License
27
+
28
+ MIT.
@@ -131,6 +131,23 @@ function parseFrontmatter(markdown) {
131
131
  body
132
132
  };
133
133
  }
134
+ function pushNested(out, inner, wrap) {
135
+ const children = parseInline(inner);
136
+ if (children.length === 0) {
137
+ out.push({
138
+ text: inner,
139
+ attrs: { ...wrap }
140
+ });
141
+ return;
142
+ }
143
+ for (const child of children) out.push({
144
+ text: child.text,
145
+ attrs: {
146
+ ...child.attrs ?? {},
147
+ ...wrap
148
+ }
149
+ });
150
+ }
134
151
  function parseInline(text) {
135
152
  const stripped = text.replace(/\{lang="[^"]*"\}/g, "").replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, "$2").replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
136
153
  const tokens = [];
@@ -179,22 +196,10 @@ function parseInline(text) {
179
196
  text: label,
180
197
  attrs: { docLink: { docId } }
181
198
  });
182
- } else if (match[10] !== void 0) tokens.push({
183
- text: match[10],
184
- attrs: { strike: true }
185
- });
186
- else if (match[11] !== void 0) tokens.push({
187
- text: match[11],
188
- attrs: { bold: true }
189
- });
190
- else if (match[12] !== void 0) tokens.push({
191
- text: match[12],
192
- attrs: { italic: true }
193
- });
194
- else if (match[13] !== void 0) tokens.push({
195
- text: match[13],
196
- attrs: { italic: true }
197
- });
199
+ } else if (match[10] !== void 0) pushNested(tokens, match[10], { strike: true });
200
+ else if (match[11] !== void 0) pushNested(tokens, match[11], { bold: true });
201
+ else if (match[12] !== void 0) pushNested(tokens, match[12], { italic: true });
202
+ else if (match[13] !== void 0) pushNested(tokens, match[13], { italic: true });
198
203
  else if (match[14] !== void 0) tokens.push({
199
204
  text: match[14],
200
205
  attrs: { code: true }
@@ -730,11 +735,19 @@ function parseBlocks(markdown) {
730
735
  function fillTextInto(el, tokens) {
731
736
  const filtered = tokens.filter((t) => t.text.length > 0);
732
737
  if (!filtered.length) return;
733
- const xtNodes = filtered.map(() => new yjs.XmlText());
734
- el.insert(0, xtNodes);
738
+ const children = filtered.map((tok) => {
739
+ return (tok.attrs?.docLink)?.docId ? new yjs.XmlElement("docLink") : new yjs.XmlText();
740
+ });
741
+ el.insert(0, children);
735
742
  filtered.forEach((tok, i) => {
736
- if (tok.attrs) xtNodes[i].insert(0, tok.text, tok.attrs);
737
- else xtNodes[i].insert(0, tok.text);
743
+ const node = children[i];
744
+ if (node instanceof yjs.XmlElement) {
745
+ const dl = tok.attrs.docLink;
746
+ node.setAttribute("docId", dl.docId);
747
+ return;
748
+ }
749
+ if (tok.attrs) node.insert(0, tok.text, tok.attrs);
750
+ else node.insert(0, tok.text);
738
751
  });
739
752
  }
740
753
  function blockElName(b) {
@@ -1090,6 +1103,15 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
1090
1103
 
1091
1104
  //#endregion
1092
1105
  //#region packages/convert/src/yjs-to-markdown.ts
1106
+ function isXElem(n) {
1107
+ return !!n && typeof n.nodeName === "string";
1108
+ }
1109
+ function isXText(n) {
1110
+ return !!n && typeof n.nodeName !== "string" && typeof n.toDelta === "function";
1111
+ }
1112
+ function localizeFragment(fragment) {
1113
+ return fragment;
1114
+ }
1093
1115
  function serializeDelta(delta) {
1094
1116
  let result = "";
1095
1117
  for (const op of delta) {
@@ -1150,12 +1172,15 @@ function serializeDelta(delta) {
1150
1172
  }
1151
1173
  function serializeInline(el) {
1152
1174
  const parts = [];
1153
- for (const child of el.toArray()) if (child instanceof yjs.XmlText) parts.push(serializeDelta(child.toDelta()));
1154
- else if (child instanceof yjs.XmlElement) parts.push(serializeInline(child));
1175
+ for (const child of el.toArray()) if (isXText(child)) parts.push(serializeDelta(child.toDelta()));
1176
+ else if (isXElem(child)) if (child.nodeName === "docLink") {
1177
+ const docId = child.getAttribute("docId") ?? "";
1178
+ parts.push(`[[${docId}]]`);
1179
+ } else parts.push(serializeInline(child));
1155
1180
  return parts.join("");
1156
1181
  }
1157
1182
  function serializeBlock(el, indent = "") {
1158
- if (el instanceof yjs.XmlText) return serializeDelta(el.toDelta());
1183
+ if (isXText(el)) return serializeDelta(el.toDelta());
1159
1184
  switch (el.nodeName) {
1160
1185
  case "documentHeader":
1161
1186
  case "documentMeta": return "";
@@ -1175,7 +1200,7 @@ function serializeBlock(el, indent = "") {
1175
1200
  }
1176
1201
  case "blockquote": {
1177
1202
  const lines = [];
1178
- for (const child of el.toArray()) if (child instanceof yjs.XmlElement) {
1203
+ for (const child of el.toArray()) if (isXElem(child)) {
1179
1204
  const text = serializeBlock(child);
1180
1205
  for (const line of text.split("\n")) lines.push(`> ${line}`);
1181
1206
  }
@@ -1236,11 +1261,11 @@ function serializeBlock(el, indent = "") {
1236
1261
  if (to) props.push(`to="${to}"`);
1237
1262
  return `::card${props.length ? `{${props.join(" ")}}` : ""}\n${serializeChildren(el)}\n::`;
1238
1263
  }
1239
- case "cardGroup": return `::card-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1240
- case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1241
- case "codeGroup": return `::code-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1264
+ case "cardGroup": return `::card-group\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1265
+ case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1266
+ case "codeGroup": return `::code-group\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1242
1267
  case "codePreview": {
1243
- const children = el.toArray().filter((c) => c instanceof yjs.XmlElement);
1268
+ const children = el.toArray().filter((c) => isXElem(c));
1244
1269
  const nonCode = children.filter((c) => c.nodeName !== "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
1245
1270
  const code = children.filter((c) => c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
1246
1271
  const parts = [nonCode];
@@ -1258,16 +1283,16 @@ function serializeBlock(el, indent = "") {
1258
1283
  if (required === true || required === "true") props.push("required=\"true\"");
1259
1284
  return `::field{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
1260
1285
  }
1261
- case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1286
+ case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
1262
1287
  default: return serializeChildren(el);
1263
1288
  }
1264
1289
  }
1265
1290
  function serializeChildren(el) {
1266
1291
  const blocks = [];
1267
- for (const child of el.toArray()) if (child instanceof yjs.XmlElement) {
1292
+ for (const child of el.toArray()) if (isXElem(child)) {
1268
1293
  const text = serializeBlock(child);
1269
1294
  if (text) blocks.push(text);
1270
- } else if (child instanceof yjs.XmlText) {
1295
+ } else if (isXText(child)) {
1271
1296
  const text = serializeDelta(child.toDelta());
1272
1297
  if (text) blocks.push(text);
1273
1298
  }
@@ -1277,11 +1302,11 @@ function serializeListItems(el, type, indent) {
1277
1302
  const lines = [];
1278
1303
  let counter = 1;
1279
1304
  for (const child of el.toArray()) {
1280
- if (!(child instanceof yjs.XmlElement) || child.nodeName !== "listItem") continue;
1305
+ if (!isXElem(child) || child.nodeName !== "listItem") continue;
1281
1306
  const prefix = type === "bullet" ? "- " : `${counter++}. `;
1282
1307
  const subParts = [];
1283
1308
  for (const sub of child.toArray()) {
1284
- if (!(sub instanceof yjs.XmlElement)) continue;
1309
+ if (!isXElem(sub)) continue;
1285
1310
  if (sub.nodeName === "bulletList") subParts.push(serializeListItems(sub, "bullet", indent + " "));
1286
1311
  else if (sub.nodeName === "orderedList") subParts.push(serializeListItems(sub, "ordered", indent + " "));
1287
1312
  else subParts.push(serializeInline(sub));
@@ -1297,13 +1322,13 @@ function serializeListItems(el, type, indent) {
1297
1322
  function serializeTaskList(el, indent) {
1298
1323
  const lines = [];
1299
1324
  for (const child of el.toArray()) {
1300
- if (!(child instanceof yjs.XmlElement) || child.nodeName !== "taskItem") continue;
1325
+ if (!isXElem(child) || child.nodeName !== "taskItem") continue;
1301
1326
  const checked = child.getAttribute("checked");
1302
1327
  const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
1303
1328
  let header = "";
1304
1329
  const nestedParts = [];
1305
1330
  for (const sub of child.toArray()) {
1306
- if (!(sub instanceof yjs.XmlElement)) continue;
1331
+ if (!isXElem(sub)) continue;
1307
1332
  if (sub.nodeName === "paragraph" && header === "") header = serializeInline(sub);
1308
1333
  else if (sub.nodeName === "bulletList") nestedParts.push(serializeListItems(sub, "bullet", indent + " "));
1309
1334
  else if (sub.nodeName === "orderedList") nestedParts.push(serializeListItems(sub, "ordered", indent + " "));
@@ -1316,16 +1341,16 @@ function serializeTaskList(el, indent) {
1316
1341
  return lines.join("\n");
1317
1342
  }
1318
1343
  function getCodeBlockText(el) {
1319
- for (const child of el.toArray()) if (child instanceof yjs.XmlText) return child.toString();
1344
+ for (const child of el.toArray()) if (isXText(child)) return child.toString();
1320
1345
  return "";
1321
1346
  }
1322
1347
  function serializeTable(el) {
1323
- const rows = el.toArray().filter((c) => c instanceof yjs.XmlElement);
1348
+ const rows = el.toArray().filter((c) => isXElem(c));
1324
1349
  if (!rows.length) return "";
1325
1350
  const serializedRows = [];
1326
1351
  for (const row of rows) {
1327
- const cells = row.toArray().filter((c) => c instanceof yjs.XmlElement).map((cell) => {
1328
- return cell.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeInline(c)).join(" ");
1352
+ const cells = row.toArray().filter((c) => isXElem(c)).map((cell) => {
1353
+ return cell.toArray().filter((c) => isXElem(c)).map((c) => serializeInline(c)).join(" ");
1329
1354
  });
1330
1355
  serializedRows.push(cells);
1331
1356
  }
@@ -1344,7 +1369,7 @@ function serializeTable(el) {
1344
1369
  ].join("\n");
1345
1370
  }
1346
1371
  function serializeSlottedContainer(el, containerName, childName, slotPrefix) {
1347
- return `::${containerName}\n${el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === childName).map((item) => {
1372
+ return `::${containerName}\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === childName).map((item) => {
1348
1373
  const label = item.getAttribute("label") ?? "";
1349
1374
  const icon = item.getAttribute("icon") ?? "";
1350
1375
  const props = [];
@@ -1394,7 +1419,7 @@ function escapeYaml(s) {
1394
1419
  return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
1395
1420
  }
1396
1421
  function serializeBlockToHtml(el) {
1397
- if (el instanceof yjs.XmlText) return serializeDeltaToHtml(el.toDelta());
1422
+ if (isXText(el)) return serializeDeltaToHtml(el.toDelta());
1398
1423
  const name = el.nodeName;
1399
1424
  switch (name) {
1400
1425
  case "documentHeader":
@@ -1408,11 +1433,11 @@ function serializeBlockToHtml(el) {
1408
1433
  case "orderedList": return `<ol>${serializeListHtml(el)}</ol>`;
1409
1434
  case "taskList": return `<ul>${serializeTaskListHtml(el)}</ul>`;
1410
1435
  case "codeBlock": {
1411
- const lang = el.getAttribute("language") ?? "";
1436
+ const lang = (el.getAttribute("language") ?? "").replace(/[^\w.-]/g, "");
1412
1437
  const code = escapeHtml(getCodeBlockText(el));
1413
1438
  return lang ? `<pre><code class="language-${lang}">${code}</code></pre>` : `<pre><code>${code}</code></pre>`;
1414
1439
  }
1415
- case "blockquote": return `<blockquote>\n${el.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlockToHtml(c)).join("\n")}\n</blockquote>`;
1440
+ case "blockquote": return `<blockquote>\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlockToHtml(c)).join("\n")}\n</blockquote>`;
1416
1441
  case "table": return serializeTableHtml(el);
1417
1442
  case "horizontalRule": return "<hr>";
1418
1443
  case "image": {
@@ -1426,13 +1451,13 @@ function serializeBlockToHtml(el) {
1426
1451
  if (uploadId) return `<!--fileblock:${uploadId}:${filename}-->`;
1427
1452
  return `<!-- file: ${filename} -->`;
1428
1453
  }
1429
- default: return `<div data-type="${name}">\n${el.toArray().filter((c) => c instanceof yjs.XmlElement || c instanceof yjs.XmlText).map((c) => c instanceof yjs.XmlElement ? serializeBlockToHtml(c) : serializeDeltaToHtml(c.toDelta())).join("\n")}\n</div>`;
1454
+ default: return `<div data-type="${name}">\n${el.toArray().filter((c) => isXElem(c) || isXText(c)).map((c) => isXElem(c) ? serializeBlockToHtml(c) : serializeDeltaToHtml(c.toDelta())).join("\n")}\n</div>`;
1430
1455
  }
1431
1456
  }
1432
1457
  function serializeInlineHtml(el) {
1433
1458
  const parts = [];
1434
- for (const child of el.toArray()) if (child instanceof yjs.XmlText) parts.push(serializeDeltaToHtml(child.toDelta()));
1435
- else if (child instanceof yjs.XmlElement) parts.push(serializeInlineHtml(child));
1459
+ for (const child of el.toArray()) if (isXText(child)) parts.push(serializeDeltaToHtml(child.toDelta()));
1460
+ else if (isXElem(child)) parts.push(serializeInlineHtml(child));
1436
1461
  return parts.join("");
1437
1462
  }
1438
1463
  function serializeDeltaToHtml(delta) {
@@ -1451,23 +1476,23 @@ function serializeDeltaToHtml(delta) {
1451
1476
  return result;
1452
1477
  }
1453
1478
  function serializeListHtml(el) {
1454
- return el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === "listItem").map((li) => `<li>${li.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeBlockToHtml(c)).join("")}</li>`).join("\n");
1479
+ return el.toArray().filter((c) => isXElem(c) && c.nodeName === "listItem").map((li) => `<li>${li.toArray().filter((c) => isXElem(c)).map((c) => serializeBlockToHtml(c)).join("")}</li>`).join("\n");
1455
1480
  }
1456
1481
  function serializeTaskListHtml(el) {
1457
- return el.toArray().filter((c) => c instanceof yjs.XmlElement && c.nodeName === "taskItem").map((ti) => {
1482
+ return el.toArray().filter((c) => isXElem(c) && c.nodeName === "taskItem").map((ti) => {
1458
1483
  const rawChecked = ti.getAttribute("checked");
1459
1484
  const checked = rawChecked === true || rawChecked === "true";
1460
- const text = ti.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeInlineHtml(c)).join("");
1485
+ const text = ti.toArray().filter((c) => isXElem(c)).map((c) => serializeInlineHtml(c)).join("");
1461
1486
  return `<li><input type="checkbox"${checked ? " checked" : ""} disabled> ${text}</li>`;
1462
1487
  }).join("\n");
1463
1488
  }
1464
1489
  function serializeTableHtml(el) {
1465
- const rows = el.toArray().filter((c) => c instanceof yjs.XmlElement);
1490
+ const rows = el.toArray().filter((c) => isXElem(c));
1466
1491
  if (!rows.length) return "";
1467
1492
  return `<table>\n${rows.map((row, ri) => {
1468
1493
  const tag = ri === 0 ? "th" : "td";
1469
- return `<tr>${row.toArray().filter((c) => c instanceof yjs.XmlElement).map((cell) => {
1470
- return `<${tag}>${cell.toArray().filter((c) => c instanceof yjs.XmlElement).map((c) => serializeInlineHtml(c)).join("")}</${tag}>`;
1494
+ return `<tr>${row.toArray().filter((c) => isXElem(c)).map((cell) => {
1495
+ return `<${tag}>${cell.toArray().filter((c) => isXElem(c)).map((c) => serializeInlineHtml(c)).join("")}</${tag}>`;
1471
1496
  }).join("")}</tr>`;
1472
1497
  }).join("\n")}\n</table>`;
1473
1498
  }
@@ -1475,6 +1500,7 @@ function escapeHtml(s) {
1475
1500
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1476
1501
  }
1477
1502
  function yjsToMarkdown(fragment, label, meta, type) {
1503
+ fragment = localizeFragment(fragment);
1478
1504
  const { text: headerText, source: titleSource } = readDocumentHeader(fragment);
1479
1505
  const effectiveTitle = headerText || label;
1480
1506
  const docMeta = readDocumentMeta(fragment);
@@ -1498,7 +1524,7 @@ function readDocumentMeta(fragment) {
1498
1524
  const meta = {};
1499
1525
  let type;
1500
1526
  for (const child of fragment.toArray()) {
1501
- if (!(child instanceof yjs.XmlElement) || child.nodeName !== "documentMeta") continue;
1527
+ if (!isXElem(child) || child.nodeName !== "documentMeta") continue;
1502
1528
  const attrs = child.getAttributes();
1503
1529
  for (const k of Object.keys(attrs)) {
1504
1530
  const v = attrs[k];
@@ -1518,8 +1544,8 @@ function readDocumentMeta(fragment) {
1518
1544
  }
1519
1545
  function readDocumentHeader(fragment) {
1520
1546
  for (const child of fragment.toArray()) {
1521
- if (!(child instanceof yjs.XmlElement) || child.nodeName !== "documentHeader") continue;
1522
- const text = child.toArray().find((c) => c instanceof yjs.XmlText);
1547
+ if (!isXElem(child) || child.nodeName !== "documentHeader") continue;
1548
+ const text = child.toArray().find((c) => isXText(c));
1523
1549
  const src = child.getAttribute("titleSource");
1524
1550
  const source = src === "h1" || src === "frontmatter" ? src : void 0;
1525
1551
  return {
@@ -1532,7 +1558,7 @@ function readDocumentHeader(fragment) {
1532
1558
  function collectBodyBlocks(fragment) {
1533
1559
  const out = [];
1534
1560
  for (const child of fragment.toArray()) {
1535
- if (!(child instanceof yjs.XmlElement)) continue;
1561
+ if (!isXElem(child)) continue;
1536
1562
  if (child.nodeName === "documentHeader" || child.nodeName === "documentMeta") continue;
1537
1563
  out.push(child);
1538
1564
  }
@@ -1567,9 +1593,10 @@ function isMetaEmpty(meta) {
1567
1593
  * accessibility tooling, search indexing, and snippet previews.
1568
1594
  */
1569
1595
  function yjsToPlainText(fragment) {
1596
+ fragment = localizeFragment(fragment);
1570
1597
  const out = [];
1571
1598
  const visit = (node) => {
1572
- if (node instanceof yjs.XmlText) {
1599
+ if (isXText(node)) {
1573
1600
  out.push(node.toString());
1574
1601
  return;
1575
1602
  }
@@ -1579,17 +1606,18 @@ function yjsToPlainText(fragment) {
1579
1606
  if (alt) out.push(alt);
1580
1607
  return;
1581
1608
  }
1582
- for (const child of node.toArray()) if (child instanceof yjs.XmlText || child instanceof yjs.XmlElement) visit(child);
1609
+ for (const child of node.toArray()) if (isXText(child) || isXElem(child)) visit(child);
1583
1610
  if (node.nodeName !== "paragraph" && node.length === 0) return;
1584
1611
  out.push("\n");
1585
1612
  };
1586
- for (const child of fragment.toArray()) if (child instanceof yjs.XmlText || child instanceof yjs.XmlElement) visit(child);
1613
+ for (const child of fragment.toArray()) if (isXText(child) || isXElem(child)) visit(child);
1587
1614
  return out.join("").replace(/\n+$/, "").replace(/\n{3,}/g, "\n\n");
1588
1615
  }
1589
1616
  function yjsToHtml(fragment, label) {
1617
+ fragment = localizeFragment(fragment);
1590
1618
  const title = escapeHtml(label);
1591
1619
  const bodyParts = [];
1592
- for (const child of fragment.toArray()) if (child instanceof yjs.XmlElement) {
1620
+ for (const child of fragment.toArray()) if (isXElem(child)) {
1593
1621
  const html = serializeBlockToHtml(child);
1594
1622
  if (html) bodyParts.push(html);
1595
1623
  }
@@ -3075,7 +3103,7 @@ function buildReverseLookup(manifest) {
3075
3103
  * e.g. "My Project!" -> "my-project"
3076
3104
  */
3077
3105
  function labelToFilename(label) {
3078
- return label.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "untitled";
3106
+ return String(label ?? "").toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "untitled";
3079
3107
  }
3080
3108
  /**
3081
3109
  * Convert a filename back to a label (best-effort).
@@ -3174,7 +3202,8 @@ function simpleHash(str) {
3174
3202
  function getTreeData(treeMap) {
3175
3203
  const data = {};
3176
3204
  treeMap.forEach((val, key) => {
3177
- if (val && typeof val === "object") data[key] = val;
3205
+ const plain = val instanceof yjs.Map || !!val && typeof val === "object" && typeof val.toJSON === "function" && typeof val.get === "function" ? val.toJSON() : val;
3206
+ if (plain && typeof plain === "object") data[key] = plain;
3178
3207
  });
3179
3208
  return data;
3180
3209
  }